Skip to content

✨ Workflow models: signed bundles, hub marketplace catalogue with nightly refresh, spoke import/publish - #17714

Merged
hivecommons-hive[bot] merged 9 commits into
v5from
workflowmodel/v5-p7-marketplace
Nov 11, 2026
Merged

hivecommons-hive[bot] merged 9 commits into
v5from
workflowmodel/v5-p7-marketplace

Conversation

@clubanderson

Copy link
Copy Markdown
Member

RFC phase 7 of the workflow-models RFC: a model can now leave the hive it was authored on. Three pieces — a signed bundle format, a hub-side immutable marketplace catalogue with nightly refresh from the CNCF corpus, and spoke import/publish/browse — plus a Settings → Workflows → Marketplace view.

Refs #17620

What's in it

A. pkg/workflowmodel/bundle (new)

  • Bundle{APIVersion, Kind, Model, Profile, Hash, CreatedAt, CreatedBy, Tier}; Export (compiles, refuses on compile errors), Encode/Decode (canonical sorted-key JSON, strict), Verify (recomputes sha256 of the canonical model and recompiles locally, comparing the profile structurally — the published profile is never trusted), CanonicalHash.
  • Tiers: built-in · curated · ingested-unreviewed · local · local-published.
  • Shared catalogue wire types IndexEntry/Index (reserve adoption, ratings, benchmarks as empty objects).
  • pkg/workflowmodel/ingest/corpus (new): the CNCF survey corpus embedded (262 projects / 299 repos) so hub and cmd/workflow-survey share one list; the survey falls back to it when no projects.json is passed.

B. Spoke API — pkg/dashboard/api_workflows_marketplace.go (new; one-line hook in api_workflows.go)

Route Role Result
GET /api/workflows/{name}/{version}/bundle any bundle JSON
POST /api/workflows/import (body = bundle) owner Verify → recompile → Save pinned to the bundle version when free else next version → 201 {entry, verified:true}; 422 on verify failure; audit workflow_import
GET /api/marketplace/workflows[?refresh=1] any hub proxy with If-None-Match + TTL cache → {index, stale, etag, fetchedAt, error?}; hub down → 502 + cached index + stale:true
POST /api/marketplace/workflows/{name}/{version}/import owner fetch bundle from hub → import as above
POST /api/marketplace/workflows/publish {name, version} owner export → POST to hub with the spoke's heartbeat bearer + X-Hive-ID → 201 {published, hash, tier}; audit workflow_publish
POST /api/marketplace/refresh[?repo=] owner forwards to hub (hub-admin gated there); audit workflow_marketplace_refresh

Config (pkg/config): workflow.marketplace.enabled (default true), workflow.marketplace.url (default hub.url), workflow.marketplace.refresh_interval (default 15m). Golden regenerated. openapi.json paths/schemas added as additive blocks.

C. Hub catalogue — pkg/hub/marketplace{,_store,_refresh}.go (new)

  • Store under the hub data dir: marketplace/workflows/<name>/<version>.bundle.json + index.json + refresh/<owner>__<repo>.json. Immutable: identical republish is a no-op, different content under an existing name@version → 409. ETag = sha256 of index bytes. Index rebuilds from bundles if index.json is missing.
  • Routes: GET /api/marketplace/workflows (ETag/304), GET /api/marketplace/workflows/{name}/{version}/bundle (immutable cache headers), POST /api/marketplace/workflows (publish; spoke heartbeat bearer + X-Hive-ID, or hub-admin session with CSRF evidence; publisher may assert local/local-published/ingested-unreviewed, curated/built-in need admin; hub re-verifies), POST /api/marketplace/refresh[?repo=] (admin; background 202 or synchronous single repo), GET /api/marketplace/status.
  • Refresh: corpus + HIVE_HUB_MARKETPLACE_EXTRA_REPOS; per repo fetch default-branch tarball via the hub's GitHub identity → ingest.ReadTree → files hash; unchanged → no-op; changed → ingest.Lifecycle → bundle.Export → publish ingested-YYYYMMDD.N tier ingested-unreviewed with source SHA. Curated never overwritten, only flagged newerIngestAvailable. Sequential crawl with pause, honours GitHub rate-limit headers. Nightly at HIVE_HUB_MARKETPLACE_REFRESH_SCHEDULE (default 03:00 UTC; empty disables) + on demand; seeds once at startup (goroutine) when the catalogue is empty and a GitHub token is present. HIVE_HUB_MARKETPLACE_DISABLED → routes 404.
  • Documented in docs/api-reference/hub-server.md.

D. UI — static-src/js/config-workflows-marketplace.js + css/workflows-marketplace.css (new)

Marketplace item in the Workflows nav; table with tier badges, search, tier filter (with counts), newer ingest available flag, reserved Adoption/Rating columns (—), Import (verified + compiled locally → toast → opens the library entry), Reload, Refresh catalogue (owner), stale banner; Publish on library rows and detail (owner, hiveConfirm). Pure render functions + node test. CSS tokens only; no toLocaleString; no forced downloads.

E. Docs

"Bundles and marketplace (RFC phase 7)" appended to docs/design/workflow-models.md; hub routes in hub-server.md; changelog fragment.

Validation

  • gofmt, go vet clean on touched packages.
  • Coverage: workflowmodel/bundle 92.4%, ingest/corpus 96.8%, config 90.1%, hub 88.8% (floor 87), dashboard 86.5% (floor 84), webstatic 98.6%.
  • go test ./pkg/dashboard/ ./pkg/dashboard/webstatic/ ./pkg/hub/ ./pkg/workflowmodel/... ./pkg/config/ — all pass except the two known macOS-only failures (TestConfigExportReportsPersistedPath, TestKnowledgeDrainLegacyMemory).
  • webstatic/gen -check, check-api-reference-citations.sh --fix (0 drifted), check-changelog-fragment.sh v5 OK.
  • Ratchets (style, forced-download, timezone helpers, openapi route parity, dashboard import allowlist, settings YAML-leaf parity) green.

Deviations / notes

  • Config lives under workflow.marketplace.* (nested in WorkflowConfig) rather than a top-level block.
  • Hub-side settings are env vars (HIVE_HUB_MARKETPLACE_*), matching how the hub is configured today; no hub YAML.
  • The hub has no audit store — publish/refresh are recorded through its structured logger.
  • Spoke→hub identity reuses the heartbeat bearer, carried with an X-Hive-ID header.
  • Default marketplace URL follows hub.url (https://hive.hivecommons.dev in config); nothing in the repo references hub.hivecommons.dev.
  • adoption/ratings/benchmarks are reserved and empty; nothing collects them yet.

@kubestellar-prow kubestellar-prow Bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Nov 11, 2026
@kubestellar-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign hanthor for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubestellar-prow kubestellar-prow Bot added needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Nov 11, 2026
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Open PR overlap check

Base branch: v5

Conflicts are checked with every PR merged onto the current tip of v5, so a PR that only needs a rebase is not reported as conflicting with its siblings.

No conflicting open PRs or same-file overlaps were found.

@clubanderson
clubanderson force-pushed the workflowmodel/v5-p7-marketplace branch from 43c3eea to f7e6701 Compare November 11, 2026 17:50
@kubestellar-prow kubestellar-prow Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Nov 11, 2026
@github-actions github-actions Bot added the conflicts-with-open-pr Open PR has a merge conflict with another open PR label Nov 11, 2026
@hivecommons-hive

hivecommons-hive Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Hive status — updated in place; edits do not notify. Full history is in the Activity Monitor.

clubanderson this PR is held at stage/071-needs-author and the hive will not merge it yet.

Why:

  • the branch conflicts with the base and needs a rebase

Next: the hive does not push to your branch. Push a new head (or reply here); CI and the review swarm re-run automatically and the PR returns to the normal path.

Evidence bundle `#17714a305b60` sha256:399199cce4dd898746af3377df56448c57c451a82b02690a8d3c2d88d2f9c8b4 — download from the dashboard or `hivectl review evidence #17714`.

Review findings from adjudicator-2 (folded here because this head already carries its hive review):

HUMAN DECISION NEEDED — clubanderson should any registered spoke be allowed to publish under the hub's own ingested-unreviewed tier (and squat any name@version permanently) on the public hub?

Security perspective, read at head a305b60 (hub marketplace*.go, hub_keys.go, saas_users_admin.go:requireAdmin, spoke api_workflows_marketplace.go, config.go, the marketplace JS). Auth plumbing is sound: bearer is bound to X-Hive-ID and generation-verified, admin routes go through CSRF + getRealAuthUser, store keys are regex-bound (marketplace_store.go:43-44) so no path traversal, UI output is esc()-wrapped throughout. Two trust-boundary points remain:

1. Spokes can impersonate the hub's crawl tier and permanently squat catalogue keys (medium)

src/pkg/hub/marketplace.go:163-166 lets a non-admin publisher assert ingested-unreviewed:

case bundle.TierLocalPublished, bundle.TierIngestedUnreviewed:

Only the hub's own refresh pipeline should ever produce that tier — it is the label the catalogue uses to mean "the hub crawled this from the CNCF corpus", and marketplace_store.go:286,301 uses it to set newerIngestAvailable on curated entries. Any hive that has heartbeated once (npsHiveRegistered, the whole public spoke population) can therefore publish kubernetes@ingested-20991231.1 with arbitrary model content and (a) show up with the hub-ingest badge, (b) flip the "newer ingest available" flag on the admin-curated entry, (c) pre-empt the real nightly ingest for that day (marketplaceNextIngestVersion just increments past it). The refresh path calls st.Publish directly (marketplace_refresh.go:242), never the HTTP handler, so the handler can refuse this tier for spokes with no loss:

	case bundle.TierLocalPublished:
	case bundle.TierIngestedUnreviewed, bundle.TierCurated, bundle.TierBuiltIn:
		if !who.admin {
			marketplaceJSONError(w, http.StatusForbidden, fmt.Sprintf("tier %q requires a hub admin", b.Tier))
			return
		}

(and drop TierIngestedUnreviewed from the spoke-side passthrough at api_workflows_marketplace.go:406 so it always sends local-published).

Separately, because the store is immutable and the PR adds no delete/unpublish route, any spoke can also squat foo@1.0.0 for any foo — a later admin curated publish gets 409 forever (marketplace_store.go:97). The PR body states immutability as a design goal, so I am flagging the consequence rather than the mechanism: at minimum an admin-only DELETE or a per-name ownership check (first publisher of name owns it, admin overrides) is needed before this is exposed on the public hub.

2. Spoke heartbeat bearer is sent to workflow.marketplace.url on every call, including anonymous GETs (low)

src/pkg/dashboard/api_workflows_marketplace.go:235-237 attaches SpokeHeartbeatKey() unconditionally in hubRequest, and config.go:343-351 lets workflow.marketplace.url point at a host other than hub.url. The index/bundle GETs are unauthenticated on the hub (marketplace.go:34-35), so the credential that lets anyone impersonate this hive to the hub (NPS, selfreport, publish) is handed to whatever host is configured, for no benefit. Suggest attaching the bearer only in handleMarketplacePublish/handleMarketplaceRefresh, and only when url.Parse(base).Host == url.Parse(c.Hub.URL).Host.

Not verified: complexity.ExtractTarGz bounds (pre-existing; used at marketplace_refresh.go:325 with marketplaceMaxTarball).

Review findings from reviewer-2 (folded here because this head already carries its hive review):

docs-currency review (head a305b60)

1. Three new hub env vars are missing from src/docs/env-vars.md (in-scope)

src/pkg/hub/marketplace_refresh.go:36-42 adds HIVE_HUB_MARKETPLACE_EXTRA_REPOS, HIVE_HUB_MARKETPLACE_REFRESH_SCHEDULE, HIVE_HUB_MARKETPLACE_DISABLED (read at :134, :143, :408). src/docs/env-vars.md:620-640 ("Keeping this reference current") requires a row for every new os.Getenv/os.LookupEnv in the same PR and notes CI only checks the opposite direction, so this won't fail a ratchet. The vars are described only in prose in workflow-models.md and hub-server.md, which operators don't consult for env inventory. Suggested rows after HIVE_HUB_DISCORD_REQUESTS_WEBHOOK (env-vars.md:441):

 | `HIVE_HUB_DISCORD_REQUESTS_WEBHOOK` | No | none | Secret Discord webhook URL for the hosted hub's request channel. … |
+| `HIVE_HUB_MARKETPLACE_DISABLED` | No | unset (enabled) | `1`/`true`/`yes` turns the hub workflow-model marketplace off: `/api/marketplace/*` routes return `404` and the nightly refresh goroutine does not start (`src/pkg/hub/marketplace_refresh.go`). |
+| `HIVE_HUB_MARKETPLACE_REFRESH_SCHEDULE` | No | `03:00` when unset | Daily UTC time (`HH:MM`) the hub re-ingests the CNCF corpus into the marketplace. An explicitly empty or invalid value disables the nightly run; on-demand `POST /api/marketplace/refresh` still works. |
+| `HIVE_HUB_MARKETPLACE_EXTRA_REPOS` | No | none | Comma-separated `owner/repo` list ingested alongside the embedded CNCF corpus (`pkg/workflowmodel/ingest/corpus`); entries are lower-cased and de-duplicated. |

2. Docs claim workflow.marketplace.enabled: false hides the Marketplace tab; the UI always renders it (in-scope)

src/docs/design/workflow-models.md:769 says false hides the tab and 404s the routes, and src/pkg/config/config.go:313 says "set false to hide the Marketplace view". But the nav list in src/pkg/dashboard/static-src/js/config-workflows-library.js:277 is static (['marketplace', 'Marketplace'] is always present), and nothing in the diff exposes the flag to the UI — with the setting off, the tab still appears and shows "Could not load the catalogue: workflow marketplace disabled…" (config-workflows-marketplace.js wfMktLoad, which reads the 404 from api_workflows_marketplace.go marketplaceEnabled() guard). Either gate the nav item or, more simply, make the docs match:

    enabled: true          # default true; false 404s the spoke /api/marketplace/* routes (the tab still renders and reports the catalogue as disabled)

and in config.go:313-314: // Enabled defaults to true; set false to return 404 from the proxy routes (the Marketplace view still renders and surfaces the error).

Everything else I checked lines up: GET /api/marketplace/status field list in hub-server.md matches marketplace.go (entries, byTier, etag, generatedAt, corpusRepos, trackedRepos, failingRepos, refreshing, schedule, lastRefresh, lastCheckedAt); tiers listed in the catalogue docs omit local consistently with publish normalising it to local-published; changelog fragment present. Note: no src/docs/api-reference/* page lists the spoke /api/workflows/* routes at base either, so the new spoke routes being only in the design doc + openapi.json is pre-existing, not introduced here.

Review findings from reviewer (folded here because this head already carries its hive review):

Style / conventions review (head a305b60). No blockers from this perspective; two consistency notes worth fixing before merge.

1. workflow.marketplace.refresh_interval breaks the config duration convention — src/pkg/config/config.go:322
Every other operator-facing duration in this package is a string parsed through parseDurationOr (config.go:3368; e.g. StageAdoptionConfig.OfflineGrace/RebaseAttemptTimeout at config.go:3132-3139, Effective* at config.go:3274-3284). This field is a raw time.Duration with a json tag, so the config export / golden renders it as an integer nanosecond count ("refresh_interval": 900000000000), and a bare YAML integer is silently read as nanoseconds. Suggested:

-	RefreshInterval time.Duration `yaml:"refresh_interval,omitempty" json:"refresh_interval,omitempty"`
+	RefreshInterval string `yaml:"refresh_interval,omitempty" json:"refresh_interval,omitempty"`
 }
@@
 func (m MarketplaceConfig) EffectiveRefreshInterval() time.Duration {
-	if m.RefreshInterval <= 0 {
-		return DefaultMarketplaceRefreshInterval
-	}
-	return m.RefreshInterval
+	return parseDurationOr(m.RefreshInterval, DefaultMarketplaceRefreshInterval)
 }

(and RefreshInterval: "1m" in workflow_marketplace_test.go:17).

2. refreshOneRepo encodes three outcomes in one string — src/pkg/hub/marketplace_refresh.go (refreshOneRepo, consumed in runMarketplaceRefresh)
It returns "" for unchanged, the literal "skipped" for skipped, and otherwise a name@version key; the caller then switches on those magic values. A tiny enum keeps the contract explicit:

+type marketplaceRepoOutcome int
+
+const (
+	marketplaceRepoUnchanged marketplaceRepoOutcome = iota
+	marketplaceRepoSkipped
+	marketplaceRepoPublished
+)
+
-func (s *HubServer) refreshOneRepo(ctx context.Context, st *marketplaceStore, rf *marketplaceRefresher, repo string) (string, error) {
+func (s *HubServer) refreshOneRepo(ctx context.Context, st *marketplaceStore, rf *marketplaceRefresher, repo string) (marketplaceRepoOutcome, string, error) {

Minor, no action needed: the doc comment // Run ingests repos (all when nil)… sits on var errMarketplaceRefreshBusy rather than on runMarketplaceRefresh; marketplaceJSONError is a same-signature alias of writeJSONError, which the rest of pkg/hub calls directly; marketplaceHiveIDHeader and short/shortHash are duplicated across dashboard, hub and bundle.

Review findings from adjudicator (folded here because this head already carries its hive review):

Correctness review (head a305b60; read bundle.go, api_workflows_marketplace.go, hub/marketplace{,_store,_refresh}.go and their tests, plus wfstore, ingest.Lifecycle, requireAdmin at merge-base).

1. A transient fetch failure defeats the "unchanged → no-op" invariant and mints duplicate ingested versions (medium)

src/pkg/hub/marketplace_refresh.go:200-204: when rf.fetch fails, the refresh record is overwritten with {Repo, CheckedAt, Error} only — the previous FilesHash/Published/SourceSHA are discarded. On the next successful run, the no-op test at :207 is prev.FilesHash == hash && prev.Error == ""; both halves now fail, so the repo goes through Lifecycle → Export → Publish again and lands a brand-new ingested-YYYYMMDD.1 whose content is byte-identical to the last published version (the hash differs only because Spec.Meta.Version differs, so Publish at marketplace_store.go:70-98 sees it as new). One rate-limit blip or GitHub 5xx on night N therefore produces a duplicate catalogue entry on night N+1 for every affected repo, and newerIngestAvailable is raised on curated entries for nothing (marketplace_store.go:272-276). TestMarketplaceRefreshNoopVsNewVersion (marketplace_test.go:402) only covers a repo that fails on every run, so this path is untested.

Suggested fix — carry the last good state through the error record and let a prior publish (not an empty error) gate the no-op:

--- a/src/pkg/hub/marketplace_refresh.go
+++ b/src/pkg/hub/marketplace_refresh.go
@@ func (s *HubServer) refreshOneRepo(
 	files, sha, err := rf.fetch(fctx, repo)
 	if err != nil {
-		_ = st.saveRefreshRecord(marketplaceRefreshRecord{Repo: repo, CheckedAt: rf.now().UTC(), Error: err.Error()})
+		// Keep the last good hash/publish so a transient failure does not
+		// force a re-ingest (and a duplicate version) on the next run.
+		rec, _ := st.refreshRecord(repo)
+		rec.Repo, rec.CheckedAt, rec.Error = repo, rf.now().UTC(), err.Error()
+		_ = st.saveRefreshRecord(rec)
 		return "", err
 	}
 	hash := marketplaceFilesHash(files)
 	rec := marketplaceRefreshRecord{Repo: repo, SourceSHA: sha, FilesHash: hash, Files: len(files), CheckedAt: rf.now().UTC()}
-	if prev, ok := st.refreshRecord(repo); ok && prev.FilesHash == hash && prev.Error == "" {
+	// Unchanged file set and either already published or legitimately
+	// skipped (no pipelines). A "does not compile" record keeps Published
+	// empty with Error set, so it is retried.
+	if prev, ok := st.refreshRecord(repo); ok && prev.FilesHash == hash && (prev.Published != "" || prev.Error == "") {
 		rec.Published = prev.Published

And a regression test in TestMarketplaceRefreshNoopVsNewVersion: run once (published), make fetch return an error for that repo once, then restore it and assert the third run reports Unchanged with len(res.Published) == 0 and Versions(name) still has one entry.

2. Spoke POST /api/marketplace/refresh can never succeed against the real hub (low, advisory)

api_workflows_marketplace.go:457-470 forwards the refresh with only the heartbeat bearer (hubRequest, :206-209), but the hub route is wrapped in requireAdmin (hub/marketplace.go:39), which resolves a hub session user — a bearer-only request is rejected, so the spoke always answers 403 and the owner-facing Refresh catalogue button in config-workflows-marketplace.js is a dead end. The PR body acknowledges the gate, so this is a UX/dead-code note rather than a blocker; consider hiding the button unless the hub's /api/marketplace/status reports the caller can refresh, or dropping the spoke route until spoke-initiated refresh is actually authorisable.

Not checked: the UI module and openapi additions beyond route parity; the CSRF behaviour of isCSRFSafe for server-to-server POSTs.

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Mutation testing

⏱️ The 900s mutation budget ran out, so 3 package(s) were not fully mutated and the results below do not cover them:

  • pkg/hub: skipped (not started)
  • pkg/workflowmodel/bundle: skipped (not started)
  • pkg/workflowmodel/ingest/corpus: skipped (not started)

❌ gremlins did not produce a report for 1 package(s), so no verdict can be derived from this run (see the job log):

  • pkg/dashboard: gremlins exited 137

1 mutant(s) in changed files are not reached by any test (NOT COVERED). A maintainer may apply the mutation/acknowledged label to accept this.

File Line Mutation
src/pkg/config/config.go 326 ARITHMETIC_BASE

Verdict: error (#17349).

@github-actions github-actions Bot removed the conflicts-with-open-pr Open PR has a merge conflict with another open PR label Nov 11, 2026
@clubanderson
clubanderson force-pushed the workflowmodel/v5-p7-marketplace branch 2 times, most recently from 33fc753 to 8aadf20 Compare November 11, 2026 18:50
@github-actions github-actions Bot added conflicts-with-open-pr Open PR has a merge conflict with another open PR and removed conflicts-with-open-pr Open PR has a merge conflict with another open PR labels Nov 11, 2026
Add pkg/workflowmodel/bundle: Export compiles a model and wraps it with
its profile and a sha256 of the canonical model JSON; Encode/Decode use
canonical (sorted-key) JSON; Verify recomputes the hash, recompiles the
model locally and compares the profile structurally so importers never
trust a published profile.

Add pkg/workflowmodel/ingest/corpus embedding the CNCF survey corpus
(262 projects / 299 repos) so the hub marketplace refresh and
cmd/workflow-survey share one list; the survey falls back to it when a
corpus directory has no projects.json.

Refs #17620

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Refs #17620

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
GET /api/workflows/{name}/{version}/bundle exports a verified bundle;
POST /api/workflows/import verifies and recompiles locally before saving
(422 on verification failure). /api/marketplace/workflows proxies the
hub index with an ETag cache and serves the cached copy with stale=true
on hub outage; .../{name}/{version}/import and .../publish move bundles
between the library and the hub with the spoke's heartbeat credential;
/api/marketplace/refresh forwards an on-demand refresh. Owner-gated and
audited where the store or hub is written. Shared index types live in
pkg/workflowmodel/bundle so hub and spoke agree on the wire shape.

Refs #17620

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
…us refresh

Adds an immutable, file-backed catalogue under the hub data dir
(marketplace/workflows/<name>/<version>.bundle.json + index.json) with
routes for the index (strong ETag / 304), per-bundle fetch, publish
(spoke heartbeat bearer or hub admin; curated/built-in tiers admin-only),
admin on-demand refresh and a status view. The refresh walks the embedded
CNCF corpus plus HIVE_HUB_MARKETPLACE_EXTRA_REPOS, records source SHA and
pipeline-file hash per repo, skips unchanged repos and publishes changed
ones as ingested-YYYYMMDD.N (tier ingested-unreviewed). Curated versions
are never overwritten; the index flags them newerIngestAvailable. A
nightly run (HIVE_HUB_MARKETPLACE_REFRESH_SCHEDULE, default 03:00 UTC)
starts with the background pollers and seeds an empty catalogue once a
GitHub token is present.

Refs #17620

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
… catalogue refresh

Adds a Marketplace item to the Settings → Workflows nav that lists the
hub catalogue through the spoke proxy (tier badges, search, tier filter,
newer-ingest flag, reserved adoption/rating columns, stale banner when
the hub is unreachable), imports entries into the local library (the
spoke verifies and recompiles; nothing is bound) and lets owners publish
a library model to the hub after confirmation. New files only, plus
one-line hooks in config-workflows-library.js and the manifest.

Refs #17620

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
…/publish (RFC phase 7)

Refs #17620

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Replace the two time.Sleep polls in the hub marketplace tests with
testutil.Eventually / direct cancel so the sleep ratchet stays at its
baseline, and remove the unused fakeHub.down field flagged by
golangci-lint.

Refs #17620

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
Refs #17620

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
…editor view

Refs #17620

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
@clubanderson
clubanderson force-pushed the workflowmodel/v5-p7-marketplace branch from 8aadf20 to a305b60 Compare November 11, 2026 19:07
@hivecommons-hive
hivecommons-hive Bot merged commit 40b1cd3 into v5 Nov 11, 2026
83 of 84 checks passed
@kubestellar-prow
kubestellar-prow Bot deleted the workflowmodel/v5-p7-marketplace branch November 11, 2026 19:13
@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution! Your PR has been merged.

We'd love to hear how your experience was: share feedback

@hivecommons-hive

Copy link
Copy Markdown
Contributor

merged by Hive trusted-author auto-merge: author @clubanderson holds role owner and GitHub permission admin on repo; CI green at a305b60.

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intent-alignment review (head a305b60)

Checked the diff against the PR body and RFC #17620. The route/tier/verify claims in the body match the hub code (src/pkg/hub/marketplace.go — publish gated to spoke bearer + X-Hive-ID or admin, curated/built-in admin-only at the tier switch, bundle.Verify before store). Two gaps between what the PR says and what it does:

1. "Signed bundles" — nothing is signed (medium, in-scope)

The title, changelog (changelog.d/added-workflowmodel-marketplace.md:1), OpenAPI summary (dashboard/openapi.json — "Export a stored workflow model as a signed bundle"), design doc (src/docs/design/workflow-models.md, "Bundles and marketplace" section), hub-server.md (/api/marketplace/workflows/{name}/{version}/bundle row: "One immutable signed bundle") and the Publish confirm dialog (src/pkg/dashboard/static-src/js/config-workflows-marketplace.js:173, "A signed bundle (model + compiled profile + hash)") all say signed. The implementation is a content hash only: Bundle has Hash = sha256 of canonical model JSON and no key, signer, or signature field (src/pkg/workflowmodel/bundle/bundle.go:58-68), and Verify only recomputes that hash and recompiles (bundle.go:157-199). Anyone who edits a model can recompute the hash, so the bundle provides integrity-in-transit and profile consistency, not authenticity. Calling it "signed" overstates a security property to operators clicking Import/Publish.

Suggested fix: rename to "hashed"/"verified" everywhere user-facing (or add a real signature in a follow-up). E.g.:

      const ok = await hiveConfirm({ title: 'Publish ' + name + '@' + version + ' to the hub?', message: 'A verified bundle (model + compiled profile + sha256 hash) is sent to the hub marketplace under this hive\'s identity as tier local-published. Catalogue entries are immutable: once published, this name@version cannot be changed, only superseded by a new version.', confirmLabel: 'Publish' });

and the same word swap in the title, changelog fragment, openapi.json summary, hub-server.md, and workflow-models.md.

2. Nightly auto-publish is outside what RFC #17620 scoped for P7 (low, in-scope, advisory)

The RFC says P7 is "Export/import bundle; hub listing. Marketplace publishing is a separate RFC" and, under Marketplace: "publishing is a human act from the library, never automatic." This PR's refreshOneRepo (src/pkg/hub/marketplace_refresh.go:247) publishes an ingested-unreviewed bundle for every changed repo in the 299-repo corpus on a nightly schedule and auto-seeds at startup when a GitHub token is present (marketplace_refresh.go:424-445). The body's "Deviations / notes" section does not call this out. Since the author owns the RFC this is likely intentional, but please either update the RFC's P7/Marketplace text or list it as a deviation in the PR body so the record matches the code.

No unrelated scope creep found; the cmd/workflow-survey corpus fallback is a reasonable consolidation for the shared list.

— hive: agent=ci-maintainer backend=copilot model=claude-fable-5.1 copilot=1.0.88

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant